package org.iamd.domain.business;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.iamd.domain.data.DataAccessException;
import org.iamd.domain.data.IData;
import org.iamd.domain.data.IDataAccess;
import org.iamd.util.CommonMsgKeys;
import org.iamd.util.Messenger;

/**
 * Бизнес обект, родител на всички бизнес обекти в приложението.
 * 
 * @author И. Атанасов
 * 
 * @param <D>
 *            Data обект за съответния бизнес обект
 * @param <L>
 *            DataAccess обект
 */
public abstract class BusinessObject<D extends IData, L extends IDataAccess<D, ? extends Object>>
		extends BusinessWorker<L> implements Comparable<BusinessObject<D, L>> {

	/** Използва се за следващо hash ид на новосъздадените обекти. */
	private static AtomicInteger ai = new AtomicInteger();

	/** Добавя се към hashCode-а, за новосъздадените обекти. */
	private int hashId;

	/** Системен идентификатор на обект. */
	private Serializable id;

	/** Флаг, сигнализиращ дали обекта е напълно зареден. */
	private boolean loaded;

	/**
	 * @see BusinessWorker#BusinessWorker()
	 */
	protected BusinessObject() throws ApplicationException {
		this.hashId = ai.incrementAndGet();
	}

	/**
	 * Конструиране на обект по подаден подобен обект.
	 * 
	 * @param gbo
	 *            инстанцията, от която се копират полетата в новата инстация
	 * 
	 * @throws ApplicationException
	 *             при неуспешно инстанциране
	 */
	protected BusinessObject(BusinessObject<D, L> gbo) throws ApplicationException {
		if (gbo != null) {
			setAllFields(gbo);
		}
	}

	/**
	 * Конструктор с data обект. Инициализира полетата на текущия с подадения data обект. Прави се
	 * shallow копие.
	 * 
	 * @param d
	 *            data обект
	 * 
	 * @see #copyProperties(IData, boolean)
	 * 
	 * @throws ApplicationException
	 *             при неуспешно инстанциране
	 */
	protected BusinessObject(D d) throws ApplicationException {
		this(d, false);
	}

	/**
	 * Конструктор с data обект. Инициализира полетата на текущия с подадения data обект. При
	 * използването на този обект се очаква да има транзакция към съответното репозитори.
	 * 
	 * @param d
	 *            data обект
	 * @param deepCopy
	 *            флаг за shallow или deep копие
	 * 
	 * @see #copyProperties(IData, boolean)
	 * 
	 * @throws ApplicationException
	 *             при неуспешно инстанциране
	 */
	protected BusinessObject(D d, boolean deepCopy) throws ApplicationException {
		if (d != null) {
			setId(d.getId());
			copyProperties(d, deepCopy);
		}
	}

	/**
	 * @return разликата в хеш кода на двата обекта
	 * 
	 * @see Comparable#compareTo(Object)
	 */
	@Override
	public int compareTo(BusinessObject<D, L> o) {
		return hashCode() - o.hashCode();
	}

	/**
	 * Два Бизнес обекта са еднакви ако те са инстанции на един и същ клас и имат един и същ
	 * идентификатор.
	 * 
	 * @see Object#equals(Object)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public final boolean equals(Object o) {
		return compareTo((BusinessObject<D, L>) o) == 0;
	}

	/**
	 * @return системен идентификатор на обект
	 */
	public Serializable getId() {
		return this.id;
	}

	/**
	 * @return сумата от хеш кода на класа, и идентификатора на обекта * 11 (ако обекта е нов се
	 *         взема временния идентификатор)
	 */
	@Override
	public final int hashCode() {
		return getClass().hashCode() + (isEditMode() ? this.id.hashCode() : this.hashId) * 11;
	}

	/**
	 * Показва дали обектът е в режим на редакция.
	 * 
	 * @return <code>true</code> при режим на редакция, <code>false</code> в противен случай
	 */
	public final boolean isEditMode() {
		return this.id != null;
	}

	/**
	 * @return дали обекта е напълно зареден
	 */
	public boolean isLoaded() {
		return this.loaded;
	}

	/**
	 * Пълно зареждане на бизнес обекта.
	 * 
	 * @return заредения обект
	 * 
	 * @throws ApplicationException
	 *             при неуспешно зареждане
	 */
	public BusinessObject<D, L> load() throws ApplicationException {
		Messenger.logInfo(CommonMsgKeys.M0009_LOAD_OBJECT_STARTED, new Object[] { toString() });
		L dao = null;
		try {
			dao = getDAO();
			dao.beginWork();

			// зареждат се основни данни
			copyProperties(getDAO().findById(getId()), true);
			dao.commitWork();
			this.loaded = true;
		} catch (BusinessException be) {
			rollbackTransaction(dao, CommonMsgKeys.M0004_LOAD_BUSS_OBJ_FAILED);
			throw be;
		} catch (ApplicationException e) {
			rollbackTransaction(dao, CommonMsgKeys.M0004_LOAD_BUSS_OBJ_FAILED);
			throw new ApplicationException(CommonMsgKeys.M0004_LOAD_BUSS_OBJ_FAILED,
					new Object[] { toString() }, e);
		}
		Messenger.logInfo(CommonMsgKeys.M0010_LOAD_OBJECT_FINISHED, new Object[] { toString() });
		return this;
	}

	/**
	 * Съхранява текущото състояние на бизнес обекта.
	 * 
	 * @throws ApplicationException
	 *             при неуспешен запис
	 */
	public void store() throws ApplicationException {
		Messenger.logInfo(CommonMsgKeys.M0011_STORE_OBJECT_STARTED, new Object[] { toString() });
		boolean saveMode = !isEditMode();
		L dao = null;
		try {
			dao = getDAO();
			dao.beginWork();
			validate();
			if (saveMode) {
				save();
			} else {
				update();
			}
			dao.commitWork();
			this.loaded = false;
		} catch (BusinessException be) {
			rollbackTransaction(dao, CommonMsgKeys.M0008_STORE_BUSS_OBJ_FAILED);
			throw be;
		} catch (ApplicationException e) {
			rollbackTransaction(dao, CommonMsgKeys.M0008_STORE_BUSS_OBJ_FAILED);
			throw new ApplicationException(CommonMsgKeys.M0008_STORE_BUSS_OBJ_FAILED,
					new Object[] { toString() }, e);
		}
		Messenger.logInfo(CommonMsgKeys.M0012_STORE_OBJECT_FINISHED, new Object[] { toString() });
	}

	/**
	 * @see Object#toString()
	 */
	@Override
	public String toString() {
		return this.id + " " + getClass().getSimpleName();
	}

	/**
	 * Прехвърля данните от data към this.
	 * 
	 * @param data
	 *            източника на данните
	 * @param deepCopy
	 *            <code>true</code> - цялостно зареждане на бизнес обект, <code>false</code> -
	 *            зареждане на основни данни
	 * 
	 * @throws ApplicationException
	 *             при неуспешно прехвърляне на данни
	 */
	protected abstract void copyProperties(D data, boolean deepCopy) throws ApplicationException;

	/**
	 * Метод за изтриване на текущия БО.
	 * 
	 * @throws DataAccessException
	 *             като се счупи нещо в репозиторито при триене
	 */
	protected void delete() throws DataAccessException {
		D vo = getDAO().findById(getId());
		getDAO().delete(vo);
	}

	/**
	 * Прави rollback на транзакция спрямо подадения Data Access обект. Ако rollback-a е неуспешен
	 * се хвърля изключение.
	 * 
	 * @param dao
	 *            Data Access обект
	 * @param msgKey
	 *            съобщение при възникнало изключение
	 * 
	 * @throws ApplicationException
	 *             при неуспешен rollback
	 */
	protected void rollbackTransaction(L dao, CommonMsgKeys msgKey) throws ApplicationException {
		try {
			if (dao != null) {
				dao.rollback();
			}
		} catch (DataAccessException e) {
			throw new ApplicationException(msgKey, new Object[] { toString() }, e);
		}
	}

	/**
	 * Метод за запис на текущия БО.
	 * 
	 * @throws DataAccessException
	 *             като се счупи нещо в репозиторито при нов запис
	 */
	protected void save() throws DataAccessException {
		D vo = getDAO().createInstance();
		setValues(vo);
		getDAO().save(vo);
		setId(vo.getId());
	}

	/**
	 * Setter за системен идентификатор на обект.
	 * 
	 * @param id
	 *            системен идентификатор на обект
	 */
	protected final void setId(Serializable id) {
		this.id = id;
	}

	/**
	 * Сетва на подаденото VO полетата от текущия бизнес обект.
	 * 
	 * @param vo
	 *            на него се сетват стойности
	 * 
	 * @throws DataAccessException
	 *             при неуспешна интеракция с репозиторито
	 */
	protected abstract void setValues(D vo) throws DataAccessException;

	/**
	 * Метод за промяна на конкретен БО
	 * 
	 * @throws DataAccessException
	 *             при неуспешна интеракция с репозиторито
	 */
	protected void update() throws DataAccessException {
		D vo = getDAO().findById(getId());
		setValues(vo);
		getDAO().update(vo);
	}

	/**
	 * Метод за валидиране на консистентност на данните от гледна точка на бизнес поведението.
	 * 
	 * @throws BusinessException
	 *             изключение, в която има информация какво точно не е консистентно в данните на
	 *             бизнес обекта
	 */
	protected abstract void validate() throws BusinessException;

	/**
	 * Итерира всички полета на класа и ги сетва на новата инстанция
	 * 
	 * @param gbo
	 *            старата инстанция за обекта
	 */
	private void setAllFields(BusinessObject<D, L> gbo) {
		List<Field> allGboFields = new ArrayList<Field>();
		Class<?> gboClazz = gbo.getClass();
		while (gboClazz != BusinessWorker.class) {
			Field[] currClazzFields = gboClazz.getDeclaredFields();
			for (Field field : currClazzFields) {
				allGboFields.add(field);
			}
			gboClazz = gboClazz.getSuperclass();
		}
		for (Field field : allGboFields) {
			try {
				if (!Modifier.isFinal(field.getModifiers())) {
					field.setAccessible(true);
					field.set(this, field.get(gbo));
				}
			} catch (Exception e) {
				// just continue with next
				continue;
			}
		}
	}

}